/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

 ****/
package org.activequant.util.calendar;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.activequant.core.types.TimeStamp;
import org.apache.log4j.Logger;

/**
 * A collection of utilities for working with calendars.<br>
 * <br>
 * <b>History:</b><br>
 * - [1 aug 2008] Created (mark)<br>
 * 
 * @author mark
 */
public class CalendarUtils {
	@SuppressWarnings("unused")
	private static final Logger log = Logger.getLogger(CalendarUtils.class);

	/**
	 * GMT Time zone<br/>
	 * public static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone("GMT");
	 */
	public static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone("GMT");

	/**
	 * Factor to convert milliseconds to nanoseconds<br/>
	 * public static final long MILLIS_TO_NANOS = 1000000L;
	 */
	public static final long MILLIS_TO_NANOS = 1000000L;
	/**
	 * Factor to convert nanoseconds to milliseconds<br/>
	 * public static final long NANOS_TO_MILLIS = MILLIS_TO_NANOS(1000000L);
	 */
	public static final long NANOS_TO_MILLIS = MILLIS_TO_NANOS;
	/**
	 * The number of nanoseconds in one day<br/>
	 * public static final long ONE_DAY_AS_NANOS = 24 * 3600 * 1000000000L;
	 */
	public static final long ONE_DAY_AS_NANOS = 24 * 3600 * 1000000000L;
	/**
	 * the last nanosecond of a day<br/>
	 * public static final long END_OF_DAY_AS_NANOS = ONE_DAY_AS_NANOS - 1 ((24*3600*1000000000L) - 1);
	 */
	public static final long END_OF_DAY_AS_NANOS = ONE_DAY_AS_NANOS - 1;
	/**
	 * Number of milliseconds in one hour<br/>
	 * public static final long HOUR_TO_MILLIS = 60 * 60 * 1000L;
	 */
	public static final long HOUR_TO_MILLIS = 60 * 60 * 1000L;
	/**
	 * Factor to convert hours to milliseconds<br/>
	 * public static final long MILLIS_TO_HOURS = HOUR_TO_MILLIS (60*60*1000L)
	 */
	public static final long MILLIS_TO_HOURS = HOUR_TO_MILLIS;
	/**
	 * Number of milliseconds in one minute<br/>
	 * public static final long MINUTES_TO_MILLIS = 60 * 1000L;
	 */
	public static final long MINUTES_TO_MILLIS = 60 * 1000L;
	/**
	 * Factor to convert minutes to milliseconds<br/>
	 * public static final long MILLIS_TO_MINUTES = MINUTES_TO_MILLIS(60*1000L);
	 */
	public static final long MILLIS_TO_MINUTES = MINUTES_TO_MILLIS;
	/**
	 * Factor to convert milliseconds to seconds<br/>
	 * public static final int MILLIS_TO_SECOND = 1000;
	 */
	public static final int MILLIS_TO_SECOND = 1000;
	/**
	 * Factor to convert a second to milliseconds<br/>
	 * public static final int SECOND_TO_MILLIS = MILLIS_TO_SECOND(1000);
	 */
	public static final int SECOND_TO_MILLIS = MILLIS_TO_SECOND;

	/**
	 * Get a time stamp truncated to midnight GMT time<br/>
	 * returns a TimeStamp with the given now(TimeStamp) truncated to midnight (GMT time)
	 * @param now
	 *            the time stamp to truncate to midnight.
	 * @return the to midnight truncated time stamp (GMT time).
	 */
	@Deprecated
	public static TimeStamp getLastMidnight(final TimeStamp now) {
		Calendar calendar = GregorianCalendar.getInstance(TIMEZONE_GMT);

		calendar.setTimeInMillis(now.getDate().getTime());

		calendar = toMidnight(calendar);
		//		log.info("calendar: " + calendarToString(calendar));

		return new TimeStamp(calendar.getTimeInMillis() * MILLIS_TO_NANOS);
	}

	/**
	 * Add an arbitrary number of days to a time stamp.<br/>
	 * returns a TimeStamp by increasing the given now(TimeStamp) by given days(int)*ONE_DAY_AS_NANOS nanoseconds.
	 * @param now
	 *            the time stamp to add the number of days to.
	 * @param days
	 *            the number of days. Can also be negative to subtract days from the time stamp.
	 * @return the time stamp with the added days.
	 */
	public static TimeStamp addDays(final TimeStamp now, final int days) {
		return new TimeStamp(now.getNanoseconds() + (days * ONE_DAY_AS_NANOS));
	}

	/**
	 * Determines if a calendar is a business day (i.e. not a weekend day).<br/>
	 * returns whether the DAY_OF_WEEK of the given calendar(Calendar) is neither SATURDAY(7) or SUNDAY(1)
	 * @param calendar
	 *            the calendar to check.
	 * @return true if the calendar is a business day, otherwise false.
	 */
	public static boolean isBusinessDay(final Calendar calendar) {
		return !(calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY || calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY);
	}

	/**
	 * Set the time part of a calendar to midnight (i.e. T00:00 000).<br/>
	 * returns the given calendar(Calendar) after setting its HOUR_OF_DAY, MINUTE, SECOND and MILLISECOND to 0.
	 * @param calendar
	 *            the calendar to set the time part for.
	 * @return the calendar with the time part set to midnight.
	 */
	public static Calendar toMidnight(final Calendar calendar) {
		calendar.set(Calendar.HOUR_OF_DAY, 0);
		calendar.set(Calendar.MINUTE, 0);
		calendar.set(Calendar.SECOND, 0);
		calendar.set(Calendar.MILLISECOND, 0);

		return calendar;
	}

	/**
	 * Get a time stamp with the time part set to the last nanosecond of the day (GMT time).<br/>
	 * returns a TimeStamp set to the last nanosecond of the day of the given now(TimeStamp) (GMT time).
	 * @param now
	 *            the time stamp to set the time part for.
	 * @return a new time stamp with the time part set to the last nanosecond of the day in GMT time.
	 */
	public static TimeStamp getEndOfDay(final TimeStamp now) {
		return new TimeStamp(getLastMidnight(now).getNanoseconds() + END_OF_DAY_AS_NANOS);
	}

	/**
	 * Get a calendar with the time part set to the last millisecond of the day.<br/>
	 * returns the given calendar(Calendar) after setting its time part to point to the last millisecond of the day.
	 * @param calendar
	 *            the calendar to set the time part for.
	 * @return the calendar with the time part set to the last millisecond of the day.
	 */
	public static Calendar toEndOfDay(final Calendar calendar) {
		calendar.set(Calendar.HOUR_OF_DAY, 23);
		calendar.set(Calendar.MINUTE, 59);
		calendar.set(Calendar.SECOND, 59);
		calendar.set(Calendar.MILLISECOND, MILLIS_TO_SECOND - 1);

		return calendar;
	}

	/**
	 * Get a calendar in GMT time from a calendar in (possibly) a different time zone.<br/>
	 * returns a GMT TimeZone Calendar equivalent to the given calendar(Calendar)
	 * @param calendar
	 *            the calendar to get a GMT calendar for.
	 * @return a calendar in GMT time set to the equivalent of the local calendar.
	 */
	public static Calendar calendarToGMT(final Calendar calendar) {
		final Calendar calendarGMT = GregorianCalendar.getInstance(TIMEZONE_GMT);
		calendarGMT.setTimeInMillis(calendar.getTimeInMillis());

		return calendarGMT;
	}

	/**
	 * Gets the time in milliseconds since the Epoch given a year, month of the year and day of the month.<br/>
	 * returns the time n milliseconds since the Epoch in GMT time for the given year(int), monthOfYear(int) and dayOfMonth(int). The time part is set to T00:00 000.
	 * @param year
	 *            the year to get the milliseconds for.
	 * @param monthOfYear
	 *            the month of the year to get the milliseconds for.
	 * @param dayOfMonth
	 *            the day of the month to get the milliseconds for.
	 * @return the milliseconds since the Epoch in GMT time representing the year, month of the year and day of the
	 *         month. The time part is set to T00:00 000.
	 */
	public static long createDayAsMillis(final int year, final int monthOfYear, final int dayOfMonth) {

		final Calendar calendar = Calendar.getInstance(TIMEZONE_GMT);
		clearAndSet(calendar, year, monthOfYear, dayOfMonth, 0, 0, 0);

		return calendar.getTimeInMillis();
	}

	/**
	 * Get a String representation of a calendar.<br/>
	 * returns a String representation of the given calendar(Calendar) in the form:<br/>
	 * "${YEAR}-${MONTH}-${DAY_OF_MONTH}T${HOUR_OF_DAY}:${MINUTE}:${SECOND} ${MILLISECOND} ${TimeZone}"
	 * @param calendar
	 *            the calendar to get the string representation for.
	 * @return the string representation of the calendar.
	 */
	public static String calendarToString(Calendar calendar) {
		final StringBuilder string = new StringBuilder();

		string.append(calendar.get(Calendar.YEAR));
		string.append("-");
		string.append(calendar.get(Calendar.MONTH));
		string.append("-");
		string.append(calendar.get(Calendar.DAY_OF_MONTH));
		string.append("T");
		string.append(calendar.get(Calendar.HOUR_OF_DAY));
		string.append(":");
		string.append(calendar.get(Calendar.MINUTE));
		string.append(":");
		string.append(calendar.get(Calendar.SECOND));
		string.append(" ");
		string.append(calendar.get(Calendar.MILLISECOND));
		string.append(" " + calendar.getTimeZone().getID());

		return string.toString();
	}

	/**
	 * Get a <code>String</code> representation of a time in milliseconds after midnight.<br/>
	 * returns a String representing the given millisAfterMidnight(long) in the given timeZone(TimeZone):<br/>
	 * "T ${HOUR_OF_DAY}:${MINUTE}:${SECOND} ${MILLISECOND} ${TimeZone}"
	 * @param millisAfterMidnight
	 *            the time in milliseconds after midnight to get the String representation for.
	 * @param timeZone
	 *            the time zone to be used for representing the String.
	 * @return the String representation of the time in milliseconds after midnight in the timezone's time zone.
	 */
	public static String timeInMillisAfterMidnightToString(final long millisAfterMidnight, final TimeZone timeZone) {
		final StringBuilder string = new StringBuilder();

		final Calendar calendar = GregorianCalendar.getInstance(timeZone);
		setTime(calendar, millisAfterMidnight);

		string.append("T ");
		string.append(calendar.get(Calendar.HOUR_OF_DAY));
		string.append(":");
		string.append(calendar.get(Calendar.MINUTE));
		string.append(":");
		string.append(calendar.get(Calendar.SECOND));
		string.append(" ");
		string.append(calendar.get(Calendar.MILLISECOND));
		string.append(" " + calendar.getTimeZone().getID());

		return string.toString();
	}

	/**
	 * Add a number of milliseconds to a calendar.<br/>
	 * returns the given calendar(Calendar) after adding the given millis(long) milliseconds to it.
	 * @param calendar
	 *            the calendar to add the milliseconds to.
	 * @param millis
	 *            the milliseconds to add (can also be negative for subtracting).
	 * 
	 * @return the calendar with the added milliseconds.
	 */
	public static Calendar addMillis(final Calendar calendar, final long millis) {
		final long newMillis = calendar.getTimeInMillis() + millis;
		calendar.setTimeInMillis(newMillis);

		return calendar;
	}

	/**
	 * Clears and sets a calendar with a year, month of year and day of month.<br/>
	 * Clears the given calendar(Calendar) and then sets its fields with the given year(int), monthOfYear(int) and dayOfMonth(int).
	 * @param calendar
	 *            the calendar to clear and set.
	 * @param year
	 *            the year to set the calendar to.
	 * @param monthOfYear
	 *            the month of the year to set the calendar to.
	 * @param dayOfMonth
	 *            the day of the month to set the calendar to.
	 */
	public static void clearAndSet(final Calendar calendar, final int year, final int monthOfYear, final int dayOfMonth) {
		clearAndSet(calendar, year, monthOfYear, dayOfMonth, 0, 0, 0, 0);
	}

	/**
	 * Clears and sets a calendar with a year, month of year, day of month, hours, minutes and seconds.<br/>
	 * Clears the given calendar(Calendar) and then sets its fields with the given year(int), monthOfYear(int), dayOfMonth(int), hours(int), minutes(int) and seconds(int).
	 * @param calendar
	 *            the calendar to clear and set.
	 * @param year
	 *            the year to set the calendar to.
	 * @param monthOfYear
	 *            the month of the year to set the calendar to.
	 * @param dayOfMonth
	 *            the day of the month to set the calendar to.
	 * @param hours
	 *            the hours of the day to set the calendar to.
	 * @param minutes
	 *            the minutes of the hour to set the calendar to.
	 * @param seconds
	 *            the seconds of the minute to set the calendar to.
	 */
	public static void clearAndSet(Calendar calendar, int year, int monthOfYear, int dayOfMonth, final int hours,
			final int minutes, final int seconds) {
		clearAndSet(calendar, year, monthOfYear, dayOfMonth, hours, minutes, seconds, 0);
	}

	/**
	 * Clears and sets a calendar with a year, month of year, day of month, hours, minutes, seconds and milliseconds.<br/>
	 * Clears the given calendar(Calendar) and then sets its fields with the given year(int), monthOfYear(int), dayOfMonth(int), hours(int), minutes(int), seconds(int) and
	 * milliseconds(int).
	 * @param calendar
	 *            the calendar to clear and set.
	 * @param year
	 *            the year to set the calendar to.
	 * @param monthOfYear
	 *            the month of the year to set the calendar to.
	 * @param dayOfMonth
	 *            the day of the month to set the calendar to.
	 * @param hours
	 *            the hours of the day to set the calendar to.
	 * @param minutes
	 *            the minutes of the hour to set the calendar to.
	 * @param seconds
	 *            the seconds of the minute to set the calendar to.
	 * @param milliseconds
	 *            the milliseconds of the second to set the calendar to.
	 */
	public static void clearAndSet(Calendar calendar, int year, int monthOfYear, int dayOfMonth, final int hours, final int minutes, final int seconds, final int milliseconds) {
		calendar.clear(); // also clears the milliseconds
		calendar.set(year, monthOfYear, dayOfMonth, hours, minutes, seconds);
		calendar.set(Calendar.MILLISECOND, milliseconds);
	}

	/**
	 * Set the time part of a calendar given the time in milliseconds after midnight in the calendar's time zone.<br/>
	 * Sets the time part of the given calendar(Calendar) with the given millisAfterMidnight(long) (milliseconds after midnight) 
	 * @param calendar
	 *            the calendar to set the time part for.
	 * @param millisAfterMidnight
	 *            the time to set in milliseconds after midnight in the calendar's time zone.
	 */
	public static void setTime(Calendar calendar, long millisAfterMidnight) {
		long millisLeft = millisAfterMidnight;
		int hours = (int) (millisLeft / MILLIS_TO_HOURS);
		millisLeft -= (hours * MILLIS_TO_HOURS);
		int minutes = (int) (millisLeft / MILLIS_TO_MINUTES);
		millisLeft -= (minutes * MILLIS_TO_MINUTES);
		int seconds = (int) (millisLeft / MILLIS_TO_SECOND);
		millisLeft -= (seconds * MILLIS_TO_SECOND);
		int millis = (int) millisLeft;

		calendar.set(Calendar.HOUR_OF_DAY, hours);
		calendar.set(Calendar.MINUTE, minutes);
		calendar.set(Calendar.SECOND, seconds);
		calendar.set(Calendar.MILLISECOND, millis);
	}
}